import {
	Box2,
	Camera,
	Color,
	Matrix3,
	Matrix4,
	Object3D,
	SRGBColorSpace,
	Vector3
} from '../three.module.min.js';
import { Projector } from '../renderers/Projector.js';
import { RenderableFace } from '../renderers/Projector.js';
import { RenderableLine } from '../renderers/Projector.js';
import { RenderableSprite } from '../renderers/Projector.js';

class SVGObject extends Object3D {

	constructor(node) {

		super();

		this.isSVGObject = true;

		this.node = node;

	}

}

class SVGRenderer {

	constructor() {

		let _renderData, _elements, _lights,
			_svgWidth, _svgHeight, _svgWidthHalf, _svgHeightHalf,

			_v1, _v2, _v3,

			_svgNode,
			_pathCount = 0,

			_precision = null,
			_quality = 1,

			_currentPath, _currentStyle;

		const _this = this,
			_clipBox = new Box2(),
			_elemBox = new Box2(),

			_color = new Color(),
			_diffuseColor = new Color(),
			_ambientLight = new Color(),
			_directionalLights = new Color(),
			_pointLights = new Color(),
			_clearColor = new Color(),

			_vector3 = new Vector3(), // Needed for PointLight
			_centroid = new Vector3(),
			_normal = new Vector3(),
			_normalViewMatrix = new Matrix3(),

			_viewMatrix = new Matrix4(),
			_viewProjectionMatrix = new Matrix4(),

			_svgPathPool = [],

			_projector = new Projector(),
			_svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');

		this.domElement = _svg;

		this.autoClear = true;
		this.sortObjects = true;
		this.sortElements = true;

		this.overdraw = 0.5;

		this.outputColorSpace = SRGBColorSpace;

		this.info = {

			render: {

				vertices: 0,
				faces: 0

			}

		};

		this.setQuality = function (quality) {

			switch (quality) {

				case 'high': _quality = 1; break;
				case 'low': _quality = 0; break;

			}

		};

		this.setClearColor = function (color) {

			_clearColor.set(color);

		};

		this.setPixelRatio = function () { };

		this.setSize = function (width, height) {

			_svgWidth = width; _svgHeight = height;
			_svgWidthHalf = _svgWidth / 2; _svgHeightHalf = _svgHeight / 2;

			_svg.setAttribute('viewBox', (- _svgWidthHalf) + ' ' + (- _svgHeightHalf) + ' ' + _svgWidth + ' ' + _svgHeight);
			_svg.setAttribute('width', _svgWidth);
			_svg.setAttribute('height', _svgHeight);

			_clipBox.min.set(- _svgWidthHalf, - _svgHeightHalf);
			_clipBox.max.set(_svgWidthHalf, _svgHeightHalf);

		};

		this.getSize = function () {

			return {
				width: _svgWidth,
				height: _svgHeight
			};

		};

		this.setPrecision = function (precision) {

			_precision = precision;

		};

		function removeChildNodes() {

			_pathCount = 0;

			while (_svg.childNodes.length > 0) {

				_svg.removeChild(_svg.childNodes[0]);

			}

		}

		function convert(c) {

			return _precision !== null ? c.toFixed(_precision) : c;

		}

		this.clear = function () {

			removeChildNodes();
			_svg.style.backgroundColor = _clearColor.getStyle(_this.outputColorSpace);

		};

		this.render = function (scene, camera) {

			if (camera instanceof Camera === false) {

				console.error('THREE.SVGRenderer.render: camera is not an instance of Camera.');
				return;

			}

			const background = scene.background;

			if (background && background.isColor) {

				removeChildNodes();
				_svg.style.backgroundColor = background.getStyle(_this.outputColorSpace);

			} else if (this.autoClear === true) {

				this.clear();

			}

			_this.info.render.vertices = 0;
			_this.info.render.faces = 0;

			_viewMatrix.copy(camera.matrixWorldInverse);
			_viewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, _viewMatrix);

			_renderData = _projector.projectScene(scene, camera, this.sortObjects, this.sortElements);
			_elements = _renderData.elements;
			_lights = _renderData.lights;

			_normalViewMatrix.getNormalMatrix(camera.matrixWorldInverse);

			calculateLights(_lights);

			// reset accumulated path

			_currentPath = '';
			_currentStyle = '';

			for (let e = 0, el = _elements.length; e < el; e++) {

				const element = _elements[e];
				const material = element.material;

				if (material === undefined || material.opacity === 0) continue;

				_elemBox.makeEmpty();

				if (element instanceof RenderableSprite) {

					_v1 = element;
					_v1.x *= _svgWidthHalf; _v1.y *= - _svgHeightHalf;

					renderSprite(_v1, element, material);

				} else if (element instanceof RenderableLine) {

					_v1 = element.v1; _v2 = element.v2;

					_v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
					_v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;

					_elemBox.setFromPoints([_v1.positionScreen, _v2.positionScreen]);

					if (_clipBox.intersectsBox(_elemBox) === true) {

						renderLine(_v1, _v2, material);

					}

				} else if (element instanceof RenderableFace) {

					_v1 = element.v1; _v2 = element.v2; _v3 = element.v3;

					if (_v1.positionScreen.z < - 1 || _v1.positionScreen.z > 1) continue;
					if (_v2.positionScreen.z < - 1 || _v2.positionScreen.z > 1) continue;
					if (_v3.positionScreen.z < - 1 || _v3.positionScreen.z > 1) continue;

					_v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
					_v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;
					_v3.positionScreen.x *= _svgWidthHalf; _v3.positionScreen.y *= - _svgHeightHalf;

					if (this.overdraw > 0) {

						expand(_v1.positionScreen, _v2.positionScreen, this.overdraw);
						expand(_v2.positionScreen, _v3.positionScreen, this.overdraw);
						expand(_v3.positionScreen, _v1.positionScreen, this.overdraw);

					}

					_elemBox.setFromPoints([
						_v1.positionScreen,
						_v2.positionScreen,
						_v3.positionScreen
					]);

					if (_clipBox.intersectsBox(_elemBox) === true) {

						renderFace3(_v1, _v2, _v3, element, material);

					}

				}

			}

			flushPath(); // just to flush last svg:path

			scene.traverseVisible(function (object) {

				if (object.isSVGObject) {

					_vector3.setFromMatrixPosition(object.matrixWorld);
					_vector3.applyMatrix4(_viewProjectionMatrix);

					if (_vector3.z < - 1 || _vector3.z > 1) return;

					const x = _vector3.x * _svgWidthHalf;
					const y = - _vector3.y * _svgHeightHalf;

					const node = object.node;
					node.setAttribute('transform', 'translate(' + x + ',' + y + ')');

					_svg.appendChild(node);

				}

			});

		};

		function calculateLights(lights) {

			_ambientLight.setRGB(0, 0, 0);
			_directionalLights.setRGB(0, 0, 0);
			_pointLights.setRGB(0, 0, 0);

			for (let l = 0, ll = lights.length; l < ll; l++) {

				const light = lights[l];
				const lightColor = light.color;

				if (light.isAmbientLight) {

					_ambientLight.r += lightColor.r;
					_ambientLight.g += lightColor.g;
					_ambientLight.b += lightColor.b;

				} else if (light.isDirectionalLight) {

					_directionalLights.r += lightColor.r;
					_directionalLights.g += lightColor.g;
					_directionalLights.b += lightColor.b;

				} else if (light.isPointLight) {

					_pointLights.r += lightColor.r;
					_pointLights.g += lightColor.g;
					_pointLights.b += lightColor.b;

				}

			}

		}

		function calculateLight(lights, position, normal, color) {

			for (let l = 0, ll = lights.length; l < ll; l++) {

				const light = lights[l];
				const lightColor = light.color;

				if (light.isDirectionalLight) {

					const lightPosition = _vector3.setFromMatrixPosition(light.matrixWorld).normalize();

					let amount = normal.dot(lightPosition);

					if (amount <= 0) continue;

					amount *= light.intensity;

					color.r += lightColor.r * amount;
					color.g += lightColor.g * amount;
					color.b += lightColor.b * amount;

				} else if (light.isPointLight) {

					const lightPosition = _vector3.setFromMatrixPosition(light.matrixWorld);

					let amount = normal.dot(_vector3.subVectors(lightPosition, position).normalize());

					if (amount <= 0) continue;

					amount *= light.distance == 0 ? 1 : 1 - Math.min(position.distanceTo(lightPosition) / light.distance, 1);

					if (amount == 0) continue;

					amount *= light.intensity;

					color.r += lightColor.r * amount;
					color.g += lightColor.g * amount;
					color.b += lightColor.b * amount;

				}

			}

		}

		function renderSprite(v1, element, material) {

			let scaleX = element.scale.x * _svgWidthHalf;
			let scaleY = element.scale.y * _svgHeightHalf;

			if (material.isPointsMaterial) {

				scaleX *= material.size;
				scaleY *= material.size;

			}

			const path = 'M' + convert(v1.x - scaleX * 0.5) + ',' + convert(v1.y - scaleY * 0.5) + 'h' + convert(scaleX) + 'v' + convert(scaleY) + 'h' + convert(- scaleX) + 'z';
			let style = '';

			if (material.isSpriteMaterial || material.isPointsMaterial) {

				style = 'fill:' + material.color.getStyle(_this.outputColorSpace) + ';fill-opacity:' + material.opacity;

			}

			addPath(style, path);

		}

		function renderLine(v1, v2, material) {

			const path = 'M' + convert(v1.positionScreen.x) + ',' + convert(v1.positionScreen.y) + 'L' + convert(v2.positionScreen.x) + ',' + convert(v2.positionScreen.y);

			if (material.isLineBasicMaterial) {

				let style = 'fill:none;stroke:' + material.color.getStyle(_this.outputColorSpace) + ';stroke-opacity:' + material.opacity + ';stroke-width:' + material.linewidth + ';stroke-linecap:' + material.linecap;

				if (material.isLineDashedMaterial) {

					style = style + ';stroke-dasharray:' + material.dashSize + ',' + material.gapSize;

				}

				addPath(style, path);

			}

		}

		function renderFace3(v1, v2, v3, element, material) {

			_this.info.render.vertices += 3;
			_this.info.render.faces++;

			const path = 'M' + convert(v1.positionScreen.x) + ',' + convert(v1.positionScreen.y) + 'L' + convert(v2.positionScreen.x) + ',' + convert(v2.positionScreen.y) + 'L' + convert(v3.positionScreen.x) + ',' + convert(v3.positionScreen.y) + 'z';
			let style = '';

			if (material.isMeshBasicMaterial) {

				_color.copy(material.color);

				if (material.vertexColors) {

					_color.multiply(element.color);

				}

			} else if (material.isMeshLambertMaterial || material.isMeshPhongMaterial || material.isMeshStandardMaterial) {

				_diffuseColor.copy(material.color);

				if (material.vertexColors) {

					_diffuseColor.multiply(element.color);

				}

				_color.copy(_ambientLight);

				_centroid.copy(v1.positionWorld).add(v2.positionWorld).add(v3.positionWorld).divideScalar(3);

				calculateLight(_lights, _centroid, element.normalModel, _color);

				_color.multiply(_diffuseColor).add(material.emissive);

			} else if (material.isMeshNormalMaterial) {

				_normal.copy(element.normalModel).applyMatrix3(_normalViewMatrix).normalize();

				_color.setRGB(_normal.x, _normal.y, _normal.z).multiplyScalar(0.5).addScalar(0.5);

			}

			if (material.wireframe) {

				style = 'fill:none;stroke:' + _color.getStyle(_this.outputColorSpace) + ';stroke-opacity:' + material.opacity + ';stroke-width:' + material.wireframeLinewidth + ';stroke-linecap:' + material.wireframeLinecap + ';stroke-linejoin:' + material.wireframeLinejoin;

			} else {

				style = 'fill:' + _color.getStyle(_this.outputColorSpace) + ';fill-opacity:' + material.opacity;

			}

			addPath(style, path);

		}

		// Hide anti-alias gaps

		function expand(v1, v2, pixels) {

			let x = v2.x - v1.x, y = v2.y - v1.y;
			const det = x * x + y * y;

			if (det === 0) return;

			const idet = pixels / Math.sqrt(det);

			x *= idet; y *= idet;

			v2.x += x; v2.y += y;
			v1.x -= x; v1.y -= y;

		}

		function addPath(style, path) {

			if (_currentStyle === style) {

				_currentPath += path;

			} else {

				flushPath();

				_currentStyle = style;
				_currentPath = path;

			}

		}

		function flushPath() {

			if (_currentPath) {

				_svgNode = getPathNode(_pathCount++);
				_svgNode.setAttribute('d', _currentPath);
				_svgNode.setAttribute('style', _currentStyle);
				_svg.appendChild(_svgNode);

			}

			_currentPath = '';
			_currentStyle = '';

		}

		function getPathNode(id) {

			if (_svgPathPool[id] == null) {

				_svgPathPool[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path');

				if (_quality == 0) {

					_svgPathPool[id].setAttribute('shape-rendering', 'crispEdges'); //optimizeSpeed

				}

				return _svgPathPool[id];

			}

			return _svgPathPool[id];

		}

	}

}

export { SVGObject, SVGRenderer };
